/*
 * Copyright (C) 2011 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.smartandroid.sa.json;

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.ArrayList;
import java.util.List;

import com.smartandroid.sa.json.ObjectNavigator.Visitor;
import com.smartandroid.sa.json.internal.$Gson$Preconditions;
import com.smartandroid.sa.json.internal.$Gson$Types;

/**
 * Visits each of the fields of the specified class using reflection
 * 
 * @author Inderjeet Singh
 * @author Joel Leitch
 * @author Jesse Wilson
 */
final class ReflectingFieldNavigator {

	private static final Cache<Type, List<FieldAttributes>> fieldsCache = new LruCache<Type, List<FieldAttributes>>(
			500);

	private final ExclusionStrategy exclusionStrategy;

	/**
	 * @param exclusionStrategy
	 *            the concrete strategy object to be used to filter out fields
	 *            of an object.
	 */
	ReflectingFieldNavigator(ExclusionStrategy exclusionStrategy) {
		this.exclusionStrategy = $Gson$Preconditions
				.checkNotNull(exclusionStrategy);
	}

	/**
	 * @param objTypePair
	 *            The object,type (fully genericized) being navigated
	 * @param visitor
	 *            the visitor to visit each field with
	 */
	void visitFieldsReflectively(ObjectTypePair objTypePair, Visitor visitor) {
		Type moreSpecificType = objTypePair.getMoreSpecificType();
		Object obj = objTypePair.getObject();
		for (FieldAttributes fieldAttributes : getAllFields(moreSpecificType,
				objTypePair.getType())) {
			if (exclusionStrategy.shouldSkipField(fieldAttributes)
					|| exclusionStrategy.shouldSkipClass(fieldAttributes
							.getDeclaredClass())) {
				continue; // skip
			}
			Type resolvedTypeOfField = getMoreSpecificType(
					fieldAttributes.getResolvedType(), obj, fieldAttributes);
			boolean visitedWithCustomHandler = visitor
					.visitFieldUsingCustomHandler(fieldAttributes,
							resolvedTypeOfField, obj);
			if (!visitedWithCustomHandler) {
				if ($Gson$Types.isArray(resolvedTypeOfField)) {
					visitor.visitArrayField(fieldAttributes,
							resolvedTypeOfField, obj);
				} else {
					visitor.visitObjectField(fieldAttributes,
							resolvedTypeOfField, obj);
				}
			}
		}
	}

	@SuppressWarnings("unchecked")
	private static Type getMoreSpecificType(Type type, Object obj,
			FieldAttributes fieldAttributes) {
		try {
			if (obj != null
					&& (Object.class == type || type instanceof TypeVariable)) {
				Object fieldValue = fieldAttributes.get(obj);
				if (fieldValue != null) {
					type = fieldValue.getClass();
				}
			}
		} catch (IllegalAccessException e) {
		}
		return type;
	}

	private List<FieldAttributes> getAllFields(Type type, Type declaredType) {
		List<FieldAttributes> fields = fieldsCache.getElement(type);
		if (fields == null) {
			fields = new ArrayList<FieldAttributes>();
			for (Class<?> curr : getInheritanceHierarchy(type)) {
				Field[] currentClazzFields = curr.getDeclaredFields();
				AccessibleObject.setAccessible(currentClazzFields, true);
				Field[] classFields = currentClazzFields;
				for (Field f : classFields) {
					fields.add(new FieldAttributes(curr, f, declaredType));
				}
			}
			fieldsCache.addElement(type, fields);
		}
		return fields;
	}

	/**
	 * Returns a list of classes corresponding to the inheritance of specified
	 * type
	 */
	private List<Class<?>> getInheritanceHierarchy(Type type) {
		List<Class<?>> classes = new ArrayList<Class<?>>();
		Class<?> topLevelClass = $Gson$Types.getRawType(type);
		for (Class<?> curr = topLevelClass; curr != null
				&& !curr.equals(Object.class); curr = curr.getSuperclass()) {
			if (!curr.isSynthetic()) {
				classes.add(curr);
			}
		}
		return classes;
	}
}
